using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Rocuna.Core.Extensions;

namespace Rocuna.Graphics.Core
{
    /// <summary>
    /// Custom Implementation for 3d models.
    /// </summary>
    public class RocunaModel : DrawableGameComponent
    {
        #region Properties


        /// <summary>
        /// Initializes a new instance of the <see cref="RocunaModel"/> class.
        /// </summary>
        /// <param name="model">The model.</param>
        /// <param name="position">The position.</param>
        /// <param name="rotation">The rotation.</param>
        /// <param name="scale">The scale.</param>
        /// <param name="game">The Game that the game component should be attached to.</param>
        public RocunaModel(Model model, Vector3 position, Vector3 rotation, Vector3 scale, Game game)
            : base(game)
        {
            Model = model;
            _modelTransforms = new Matrix[model.Bones.Count];
            Model.CopyAbsoluteBoneTransformsTo(_modelTransforms);
            BuildBoundingSphere();
            GenerateTags();
            Position = position;
            Rotation = rotation;
            Scale = scale;
            DrawOrder = 20000;
        }

        /// <summary>
        /// Gets or sets the model position.
        /// </summary>
        /// <value>
        /// The model position.
        /// </value>
        public Vector3 Position { get; set; }

        /// <summary>
        /// Gets or sets the model rotation.
        /// </summary>
        /// <value>
        /// The model rotation.
        /// </value>
        public Vector3 Rotation { get; set; }

        /// <summary>
        /// Gets or sets the model scale.
        /// </summary>
        /// <value>
        /// The model scale.
        /// </value>
        public Vector3 Scale { get; set; }

        /// <summary>
        /// Gets the model.
        /// </summary>
        public Model Model { get; private set; }

        /// <summary>
        /// Gets or sets the rendering camera.
        /// </summary>
        /// <value>
        /// The rendering camera.
        /// </value>
        public Camera RenderingCamera { get; set; }

        /// <summary>
        /// Gets the whole model bounding sphere.
        /// </summary>
        public BoundingSphere BoundingSphere
        {
            get
            {
                var worldTransform = Matrix.CreateScale(Scale) * Matrix.CreateTranslation(Position);
                var transformedBoundingSphere = _boundingSphere;
                return transformedBoundingSphere.Transform(worldTransform);
            }
        }

        #endregion

        #region Fields

        private readonly Matrix[] _modelTransforms;

        private BoundingSphere _boundingSphere;

        #endregion

        #region Override methods

        protected override void LoadContent()
        {
            RenderingCamera = Game.GetService<GraphicManager>().CurrentCamera;
            base.LoadContent();
        }

        public override void Draw(GameTime gameTime)
        {
            if (!this.RenderingCamera.BoundingVolumeIsInView(this.BoundingSphere))
                return;

            var baseWorld = Matrix.CreateScale(Scale) *
                Matrix.CreateFromYawPitchRoll(Rotation.Y, Rotation.X, Rotation.Z)
                            * Matrix.CreateTranslation(Position);
            foreach (var mesh in Model.Meshes)
            {
                var localWorld = _modelTransforms[mesh.ParentBone.Index] * baseWorld;
                foreach (var meshPart in mesh.MeshParts)
                {
                    var effect = meshPart.Effect;
                    if (effect is BasicEffect)
                    {
                        ((BasicEffect)effect).World = localWorld;
                        ((BasicEffect)effect).View = RenderingCamera.View;
                        ((BasicEffect)effect).Projection = RenderingCamera.Projection;

                        ((BasicEffect)effect).EnableDefaultLighting();
                    }
                    else
                    {
                        SetEffectParameter(effect, "World", localWorld);
                        SetEffectParameter(effect, "View", RenderingCamera.View);
                        SetEffectParameter(effect, "Projection", RenderingCamera.Projection);
                        SetEffectParameter(effect, "CameraPosition", RenderingCamera.Position);
                    }
                }
                mesh.Draw();
            }
        }

        public override void Update(GameTime gameTime)
        {
            RenderingCamera = Game.GetService<GraphicManager>().CurrentCamera;
            base.Update(gameTime);
        }

        #endregion

        #region Private methods

        private void BuildBoundingSphere()
        {
            var boundingSphere = new BoundingSphere(Vector3.Zero, 0);

            foreach (var mesh in Model.Meshes)
            {
                var transformed = mesh.BoundingSphere.Transform(_modelTransforms[mesh.ParentBone.Index]);
                boundingSphere = BoundingSphere.CreateMerged(boundingSphere, transformed);
            }
            _boundingSphere = boundingSphere;
        }

        private void GenerateTags()
        {
            foreach (var mesh in Model.Meshes)
            {
                foreach (var meshPart in mesh.MeshParts)
                {
                    if (meshPart.Effect is BasicEffect)
                    {
                        var effect = (BasicEffect)meshPart.Effect;
                        var tag = new MeshTag(effect.DiffuseColor, effect.Texture, effect.SpecularPower);
                        meshPart.Tag = tag;
                    }
                }
            }
        }

        private void SetEffectParameter(Effect effect, string paramName, object val)
        {
            if (effect.Parameters[paramName] == null)
                return;

            if (val is Vector3)
                effect.Parameters[paramName].SetValue((Vector3)val);
            if (val is bool)
                effect.Parameters[paramName].SetValue((bool)val);
            if (val is Matrix)
                effect.Parameters[paramName].SetValue((Matrix)val);
            if (val is Texture2D)
                effect.Parameters[paramName].SetValue((Texture2D)val);
        }

        #endregion

        #region Methods

        /// <summary>
        /// Caches the effects.
        /// </summary>
        public void CacheEffects()
        {
            foreach (var mesh in Model.Meshes)
            {
                foreach (var meshPart in mesh.MeshParts)
                {
                    ((MeshTag)meshPart.Tag).CachedEffect = meshPart.Effect;
                }
            }
        }

        /// <summary>
        /// Restores the effects.
        /// </summary>
        public void RestoreEffects()
        {
            foreach (var mesh in Model.Meshes)
            {
                foreach (var meshPart in mesh.MeshParts)
                {
                    if (((MeshTag)meshPart.Tag).CachedEffect != null)
                        meshPart.Effect = ((MeshTag)meshPart.Tag).CachedEffect;
                }

            }
        }

        /// <summary>
        /// Sets the model effect.
        /// </summary>
        /// <param name="effect">The effect.</param>
        /// <param name="copyEffect">if set to <c>true</c> [copy effect].</param>
        public void SetModelEffect(Effect effect, bool copyEffect)
        {
            foreach (var mesh in Model.Meshes)
            {
                foreach (var part in mesh.MeshParts)
                {
                    var toSet = effect;

                    if (copyEffect)
                        toSet = effect.Clone();
                    var tag = ((MeshTag)part.Tag);
                    if (tag.Texture != null)
                    {
                        SetEffectParameter(toSet, "BasicTexture", tag.Texture);
                        SetEffectParameter(toSet, "TextureEnabled", true);
                    }
                    else
                        SetEffectParameter(toSet, "TextureEnabled", false);

                    SetEffectParameter(toSet, "DifuseColor", tag.Color);
                    SetEffectParameter(toSet, "SpecularPower", tag.SpecularPower);

                    part.Effect = toSet;
                }
            }
        }
        #endregion
    }
}